chore: sampler serialization tests - #250
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #250 +/- ##
==========================================
- Coverage 93.43% 91.76% -1.67%
==========================================
Files 15 15
Lines 1432 1458 +26
==========================================
Hits 1338 1338
- Misses 94 120 +26
🚀 New features to boost your workflow:
|
|
Sorry, one other thing, we also want to make sure that simply making a copy of the samplers works. This is different than pickling |
|
|
||
| def advance_round_trip_indices(seed: int) -> list[int]: | ||
| sampler = _make_sampler(kind, seed, n_obs) | ||
| collect_indices(sampler, n_obs) # advance the rng one pass |
There was a problem hiding this comment.
Why do we need to advance the rng first here and below?
There was a problem hiding this comment.
Because this also tests randomness. For example maybe we override the rng to always to np.default(0). The tests would still pass otherwise.
I mean we already test randomness somewhere else but I wrote it in the very small chance that the seed I set and whatever seed could be set in the case of such a faulty override would be the same
There was a problem hiding this comment.
I don't follow - if you provide a seed explicitly, why does it matter how many times you advance state before/after making a copy?
There was a problem hiding this comment.
Yeah you are right sorry.
| assert restored_indices == advance_round_trip_indices(seed=0) | ||
| assert restored_indices != advance_round_trip_indices(seed=1) |
ilan-gold
left a comment
There was a problem hiding this comment.
https://docs.python.org/3/library/copy.html#object.__copy__ + https://docs.python.org/3/library/copy.html#object.__deepcopy__
In order to ensure we aren't doing anything funny, it would be probably best to implement __copy__, __deepcopy__ and __eq__ methods on our samplers.
ilan-gold
left a comment
There was a problem hiding this comment.
What about #250 (review)?
|
What about #250 (review)? Did you mean to link this:
it guards against an ignored/hard-coded rng. Otherwise the tests would still pass |
Why? If our attributes don't support copy they would fail anyway. Maybe |
|
As discussed yesterday, error on |
|
The problem with By default python would've compare references. So it would be a stricter check, even though the current function would give somewhat better sense of structural equality if a subclass has an attribute that doesn't implement To avoid it we'd need def _attr_equal(a: object, b: object) -> bool:
if isinstance(a, np.random.Generator) or isinstance(b, np.random.Generator):
return (isinstance(a, np.random.Generator) and isinstance(b, np.random.Generator)
and a.bit_generator.state == b.bit_generator.state)
if isinstance(a, Sampler) or isinstance(b, Sampler):
return a == b
if isinstance(a, np.ndarray) or isinstance(b, np.ndarray):
return isinstance(a, np.ndarray) and isinstance(b, np.ndarray) and bool(np.array_equal(a, b))
if hasattr(a, "equals") and hasattr(b, "equals") and type(a) is type(b): # pandas
return bool(a.equals(b))
if isinstance(a, int | float | bool | str | bytes | slice | tuple | frozenset | type(None)):
return type(a) is type(b) and bool(a == b)
raise TypeError(
f"Sampler equality doesn't know how to compare {type(a).__name__!r} state. "
"Extend _attr_equal or override __eq__ on the subclass that added this attribute."
)But then this would fail only when |
ilan-gold
left a comment
There was a problem hiding this comment.
Sorry if this was not clear, I thought we had discussed this in-person
| _mask: slice = slice(0, None) | ||
| _rng: np.random.Generator | None = None | ||
|
|
||
| def __eq__(self, other: object) -> bool: |
There was a problem hiding this comment.
Maybe this got lost in translation, but I think this should just be implemented custom per-implementation (so you don't have to handle every case as above and what constitutes "equal" is clear for every individual Sampler) - for now, you can make it an optional overload, but warn that in the future, it will become part of the abstract methods required
| "Use copy.deepcopy() instead." | ||
| ) | ||
|
|
||
| def __deepcopy__(self, memo: dict[int, Any]) -> Self: |
There was a problem hiding this comment.
Same thing as __eq__ here
| if hasattr(a, "equals") and hasattr(b, "equals") and type(a) is type(b): | ||
| return bool(a.equals(b)) |
There was a problem hiding this comment.
maybe you mean this? otherwise, comparing two different ExtensionArrays will crash.
| if hasattr(a, "equals") and hasattr(b, "equals") and type(a) is type(b): | |
| return bool(a.equals(b)) | |
| if hasattr(a, "equals") and hasattr(b, "equals"): | |
| return type(a) is type(b) and bool(a.equals(b)) |
There was a problem hiding this comment.
This function was one of the reasons I suggested moving to a per-class model: #250 (comment) It will make comparisons of the constituent parts much clearer, at this risk of the occasional repeated line of code.
Right this would be the other option to what I'm describing. Add a dumb default, document it, and tell people to override if they need. But blindly taking everything from |
|
First of all, nice to see you back :) One thing I'd like to separate: why do we care about About the rest I still have questions ,
Okay, I can do that ofc. But again if someone inherits those classes it will break silently (unless we make deepcopy uninheritable for child classes somehow, but do we really want that)?
It is for sure. But I can't think of an unproblematic way to do this. The best you can do is probably this: we have a registry of classes in which we support and guarantee (ie deepcopyable classes) (it should be a list of classes, the other solution is to have a list of attributes which is super unusual and probably more error prone as well). Under Why isn't documenting and proper unit tests not enough here to ensure our guarantees? Like if the pickled bin files are byte identical of samplers, what more can we guarantee? |
That was implied yeah, I would be fine making it an abstract method or raising
I want to be 100% sure that
Just enforcing that people have to implement it?
See above, my idea was that "two samplers are equal if the produce the same iteration output" which is the same as "do they have the same relevant state" but not "do they have the same state completely." Kind of like how |
Sorry I should've been more clear, I meant preventing from inheriting a Sampler's child class''s implementation of deepcopy or |
These tests ensure that the sampler and their rng states are serializable. This will also help us catch if any changes we make to the samplers keep them serializable or not. This will be more useful in my following PR's when I might add classes that might test serialization assumption